/**
 * 
 */
package org.statewalker.fsm.providers;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;

/**
 * @author kotelnikov
 */
public abstract class FsmBasicStateProvider<SK, S, EK, E>
    extends
    AbstractFsmStateProvider<SK, S, EK, E> {

    private Stack<SK> fConfigurationStack = new Stack<SK>();

    /**
     * This map defines transitions between sub-states of all managed states.
     * The structure of this map:
     * 
     * <pre>
     *     1       2       3   4 
     * Map<SK, Map<SK, Map<EK, SK>>>
     * {        
     *      1:parentStateKey = {
     *          2:sourceStateKey = {
     *              3:eventKey = 4:targetStateKey
     *          }
     *      }
     *  }
     * </pre>
     * <ol>
     * <li>parentStateKey - the identifier of the parent state; Transitions are
     * defined between substates of this state</li>
     * <li>sourceStateKey - the identifier of the starting point of the
     * transition.</li>
     * <li>eventKey - the key of the event initiating the transition</li>
     * <li>targetStateKey - the key of the target state for the current
     * transition</li>
     * </ol>
     */
    private Map<SK, Map<SK, Map<EK, SK>>> fMap = new HashMap<SK, Map<SK, Map<EK, SK>>>();

    public FsmBasicStateProvider(
        SK initialStateKey,
        SK finalStateKey,
        EK defaultEvent) {
        super(initialStateKey, finalStateKey, defaultEvent);
    }

    /**
     * @param descriptor
     */
    public void addTransition(SK parent, SK from, EK event, SK to) {
        if (from == null)
            from = fInitialStateKey;
        if (to == null)
            to = fFinalStateKey;
        Map<SK, Map<EK, SK>> transitionMap = getTransitionMap(parent, true);
        Map<EK, SK> transitions = transitionMap.get(from);
        if (transitions == null) {
            transitions = new LinkedHashMap<EK, SK>();
            transitionMap.put(from, transitions);
        }
        transitions.put(event, to);
    }

    public void beginState(SK state) {
        fConfigurationStack.push(state);
    }

    public void endState() {
        fConfigurationStack.pop();
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof FsmBasicStateProvider<?, ?, ?, ?>))
            return false;
        FsmBasicStateProvider<?, ?, ?, ?> descriptor = (FsmBasicStateProvider<?, ?, ?, ?>) obj;
        return fMap.equals(descriptor.fMap);
    }

    /**
     * @see org.statewalker.fsm.providers.AbstractFsmStateProvider#getNextSubstateKey(java.lang.Object,
     *      java.lang.Object, java.lang.Object)
     */
    @Override
    protected SK getNextSubstateKey(S parent, SK stateKey, EK eventKey) {
        SK parentKey = getStateKey(parent);
        Map<SK, Map<EK, SK>> transitionMap = getTransitionMap(parentKey, false);
        if (transitionMap == null)
            return null;
        Map<EK, SK> transitions = transitionMap.get(stateKey);
        if (transitions == null)
            return null;
        SK targetKey = transitions.get(eventKey);
        return targetKey;
    }

    protected Map<SK, Map<EK, SK>> getTransitionMap(SK parent, boolean create) {
        Map<SK, Map<EK, SK>> map = fMap.get(parent);
        if (map == null && create) {
            map = new LinkedHashMap<SK, Map<EK, SK>>();
            fMap.put(parent, map);
        }
        return map;
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return fMap.hashCode();
    }

    public void onTransition(SK from, EK event, SK to) {
        SK state = fConfigurationStack.peek();
        if (state == null)
            return;
        addTransition(state, from, event, to);
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return fMap.toString();
    }

}
